当然我在扯淡

一个失败的现代Web项目和其引发的关于Web Dev的思考

事情的起因其实很简单,就是我那天想给自己做个博客,参考的是几个二次元独立博客。本来这是 create-next-app 五分钟就该结束的事,但是我多想了一句:

> 顺便做成一个 anti-SSG 框架吧。

「框架」这个词一旦从嘴里说出来,后面的工程就开始自动繁殖。我先是写了一个 CLI,因为框架嘛肯定要有分发方式,而且 npm install 太重了,我希望用户 curl 一下就能跑:


curl -fsSL lunea.dev/init | sh
node lunea.mjs init my-blog

为了让这玩意成立,我写了 esbuild 的打包脚本,把整个 CLI 加上模板内联进一个 15KB 的 .mjs 文件里,纯粹自包含,单文件分发,干干净净。这部分写完真的能跑,我还挺得意的,node lunea.mjs init test 一敲,项目就出来了,next dev 一启,本地服务就有了。

然后我又开始想组件库。npm 包又太重了对吧(注意我又在重复这套自我催眠),所以我对标 shadcn,搞「源码 copy 进用户仓库」的范式:


node lunea.mjs add post-card
node lunea.mjs add cosmic-background

add 命令的实现我其实一行都没写,但我已经在脑子里把未来六个组件的 API 都设计完了。

回头看那一刻其实信号特别明显:

1. 我从来没用过 lunea init,除了在 /tmp/ 里测试那一次

2. templates/default/ 里只有一份极其简陋的脚手架

3. 真正花了我时间的所有东西——15 篇 pseudo posts、cosmic 渐变背景、tilt 卡片、glitch hero、navbar、archive、timeline、留言板、友链、关于页、RSS——全是直接在 /tmp/lunea-test/ 那个测试项目里写的

也就是说,框架那一层包装,一行代码都没产生美学价值。所有的设计、视觉、内容,全部来自直接编辑那个具体项目。我所谓的「框架」是给一个根本不存在的「下一个用户」准备的。

拆掉


rm -rf build.mjs cli lunea.mjs templates

/tmp/lunea-test/ 里所有真正写过的东西移进主仓库。npm installnpm run dev,和拆之前一模一样。唯一的区别是 package.json 不叫 lunea 了,它就是个 Next.js 项目,它再也不假装自己是别的什么。

讽刺的是所有真正有用的东西都还在:anti-SSG 这个想法仍然成立(所有页面 export const dynamic = 'force-dynamic',运行时 MDX 编译),shadcn 范式也还在(src/components/lunea/ 里组件源码都在仓库里),视觉设计一个像素都没掉。没有人能从外面看出来这套以前是个「框架」——从一开始它就该是这样。

关于Next.js

我之前从来没写过现代Web,所以这次其实是想试试看现代的Web框架,比说Next或者Nuxt。这次折腾虽然最后好像归零了,不过至少稍微搞清楚了一点Next的架构,我觉得还是十分巧妙的:

.next/server/app/ 这个目录下全都是 JS 函数。每一次浏览器请求进来,server 都要现场跑一遍那个函数才能生成 HTML。所谓 App Router 本质上就是一张路由表,把 URL 映射到对应的 JS 函数(page.js / route.js),routes-manifest 负责查表,RSC runtime 负责执行,React 负责把树渲染成字符串,最后流式吐回去。

next start 跑起来的那一个 Node 进程,其实同时是好几个东西

Rails 那个年代部署一个站要的 puma + sidekiq + redis + nginx + memcached 那一整套,在 Next 里塌缩成了一个 JS 进程。这是我觉得最巧妙的地方——它不是把后端做轻了,而是把后端原本散在五六个进程里的职责全部吸进了同一个运行时。代价当然有:单进程瓶颈、内存吃得凶(光 V8 + Next runtime 起步就一两百兆)、横向扩展只能多开进程或者上容器编排。但对于个人博客这种规模这些代价完全可以忽略。

这个认知带来的最大变化是部署逻辑彻底变了。我之前想着「随便扔个静态托管不就完了」,GitHub Pages、Netlify Drop、纯 S3 这些。但 Next 不是「网站」,它是「一个会响应 HTTP 请求的 JS 程序」,所以你要找的关键词应该是 Edge Functions / Serverless / SSR support,不是「静态托管」。Vercel 当然最贴合(亲爹嘛),但 Fly.io、Railway、自建 VPS 加 systemd + nginx 反代都能跑。Cloudflare Pages 走的是 Workers 那条路(V8 isolate 而不是 Node),需要适配器和一些代码改造,但也能 work。

唯一一个看上去免费又能用的偷懒路径是 output: 'export' 编译成纯静态——但这条路会让 anti-SSG 的所有特性当场失效(force-dynamic 全废、route handler 直接编译报错、每加一篇文章都要 rebuild + redeploy),那其实就是把 Next 退化成 Hexo / Astro 了,那你直接用 Hexo / Astro 不就完了

小结

第一件:做框架的最小成立条件,是有至少 3 个真实用户在用它。不是 3 个「可能用」,是 3 个「现在就在用」。我自己一个都不算(我只用它生成了那一个测试项目),我所谓的用户是一个想要 anti-SSG 博客但又不想直接用 Next 的人,我没见过这个人,我没问过这个人,这个人很可能根本不存在。shadcn 之所以成立,是因为它先解决了作者自己的问题,然后被几千个有同样问题的人认领了。它不是先有「框架愿景」再去找用户的。

第二件:Next.js 这个东西已经足够「框架」了。你想要的所有 anti-SSG 行为,它原生就支持。你以为你在它外面加了一层抽象,其实你只是在它前面塞了一层 dispatcher 而已,这层 dispatcher 在你自己一个人用的时候是负价值的。